讓我們建立一個檔案來寫帶有巨集 (macro) 的模組
defmodule MyMacro do
defmacro macro_rem(a, b) do
quote do
rem(unquote(a), unquote(b))
end
end
end
存到檔案 (my_macro.ex
) 後,使用 iex my_macro.ex
打開並載入 iex
(如果是放在 mix 專案內,使用 iex -S mix
)
在使用巨集前,我們必須要 require 該模組
require MyMacro
MyMacro.test
#=> 3
defmacro/2
使用 defmacro 定義函數時,回傳值必須要是 AST 表達式
上面我們使用了quote do: rem(unquote(a), unquote(b))
經過 quote 與 unquote 帶入的變數(假如是 3, 2) 變成{:rem, [context: Elixir, imports: [{2, Kernel}]], [3, 2]}
並在 macro 使用時從表達式執行
但是目前這樣其實還不如直接呼叫 rem 就可以了
但因為先解開成為 AST
這代表我們可以從 AST 結構上去改變執行方式
我們使用 IO.inspect/2
觀察一下這個巨集執行前的 AST
defmacro play(do: block) do
ast = quote do: unquote(block)
IO.inspect(ast)
end
require MyMacro
MyMacro.play do
name = "Jack"
name <> "White"
end
這個 AST 一樣是三個元件組成的 tuple
第三個裡面是 list 裝著在這個 block 裡面的每一行各自的 AST
{:__block__, [line: 1],
[
{:=, [line: 3], [{:name, [line: 3], nil}, "Jack"]},
{:<>, [line: 4], [{:name, [line: 4], nil}, "White"]}
]}
有順序就代表我們可以把它倒過來執行
defmacro rewind(do: block) do
{name, meta, args} = quote do: unquote(block)
{name, meta, Enum.reverse(args)}
end
在這個 rewind 函式內,我們用 pattern matching 將第三個代表 block 的 list 取出
使用 Enum.reverse/1
更改順序後再組裝回去
我們就得到了一個在 do end 之間從最後一行開始執行到前面的神奇函式
require MyMacro
MyMacro.rewind do
"#{fullname} 覺得這個是不是來亂的"
fullname = name <> " White"
name = "Jack"
IO.puts "接著是這行"
IO.puts "這行先執行"
end